//
//  LYConnectionPool.swift
//  RxSwift_Practice
//
//  Created by 李永彬 on 2020/4/28.
//  Copyright © 2020 李永彬. All rights reserved.
//

import Foundation
import CoreBluetooth

internal class LYConnectionPool: LYCBCentralManagerConnectionDelegate {
    
    // MARK: Enums

    internal enum LYError: Error {
        case noCentralManagerSet
        case alreadyConnected
        case alreadyConnecting
        case connectionByUUIDIsNotImplementedYet
        case interrupted
        case noConnectionAttemptForRemotePeripheral
        case noConnectionForRemotePeripheral
        case timeoutElapsed
        case `internal`(underlyingError: Error?)
    }
    
    // MARK: Properties
    
    internal weak var centralManager: CBCentralManager!
    internal var connectedRemotePeripherals = [LYRemotePeripheral]()
    private var connectionAttempts = [LYConnectionAttempt]()
    
    func connectWithTimeout(_ timeout: TimeInterval, remotePeripheral: LYRemotePeripheral, completionHandler: @escaping ((_ peripheralEntity: LYRemotePeripheral, _ error: Error?) -> Void)) throws {
        
        guard centralManager != nil else {
            throw LYError.noCentralManagerSet
        }
        guard !connectedRemotePeripherals.contains(remotePeripheral) else {
            throw LYError.alreadyConnected
        }
        guard !connectionAttempts.map({ connectionAttempt in return connectionAttempt.remotePeripheral }).contains(remotePeripheral) else {
            throw LYError.alreadyConnecting
        }
        guard remotePeripheral.peripheral != nil else {
            throw LYError.connectionByUUIDIsNotImplementedYet
        }
        LYCentral.shared.state = .LYBluetoothStateConnecting
        
        let timer = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(LYConnectionPool.timerElapsed(_:)), userInfo: nil, repeats: false)
        remotePeripheral.prepareForConnection()
        connectionAttempts.append(LYConnectionAttempt(remotePeripheral: remotePeripheral, timer: timer, completionHandler: completionHandler))
        centralManager!.connect(remotePeripheral.peripheral!, options: nil)
    }
    
    internal func disconnectRemotePeripheral(_ remotePeripheral: LYRemotePeripheral) throws {
        let connectedRemotePeripheral = connectedRemotePeripherals.filter({ $0 == remotePeripheral }).last
        guard connectedRemotePeripheral != nil else {
            throw LYError.noConnectionForRemotePeripheral
        }
        connectedRemotePeripheral?.unsubscribe()
        centralManager.cancelPeripheralConnection(remotePeripheral.peripheral!)
    }
    
    internal func reset() {
        for connectionAttempt in connectionAttempts {
            failConnectionAttempt(connectionAttempt, error: .interrupted)
        }
        connectionAttempts.removeAll()
        connectedRemotePeripherals.removeAll()
    }
    
    // MARK: Private Functions
    
    private func connectionAttemptForRemotePeripheral(_ remotePeripheral: LYRemotePeripheral) -> LYConnectionAttempt? {
        return connectionAttempts.filter({ $0.remotePeripheral == remotePeripheral }).last
    }
     
    private func connectionAttemptForTimer(_ timer: Timer) -> LYConnectionAttempt? {
        return connectionAttempts.filter({ $0.timer == timer }).last
    }
      
    private func connectionAttemptForPeripheral(_ peripheral: CBPeripheral) -> LYConnectionAttempt? {
        return connectionAttempts.filter({ $0.remotePeripheral.peripheral == peripheral }).last
    }

    @objc private func timerElapsed(_ timer: Timer) {
        guard connectionAttemptForTimer(timer) != nil else {
            return;
        }
        failConnectionAttempt(connectionAttemptForTimer(timer)!, error: .timeoutElapsed)
    }
    
    private func failConnectionAttempt(_ connectionAttempt: LYConnectionAttempt, error: LYError) {
        connectionAttempts.remove(at: connectionAttempts.firstIndex(of: connectionAttempt)!)
        connectionAttempt.timer.invalidate()
        if let peripheral = connectionAttempt.remotePeripheral.peripheral {
            centralManager.cancelPeripheralConnection(peripheral)
        }
        connectionAttempt.completionHandler(connectionAttempt.remotePeripheral, error)
    }
    
    private func succeedConnectionAttempt(_ connectionAttempt: LYConnectionAttempt) {
        connectionAttempt.timer.invalidate()
        connectionAttempts.remove(at: connectionAttempts.firstIndex(of: connectionAttempt)!)
        connectedRemotePeripherals.append(connectionAttempt.remotePeripheral)
        connectionAttempt.remotePeripheral.discoverServices()
        connectionAttempt.completionHandler(connectionAttempt.remotePeripheral, nil)
    }
        
    // MARK: CentralManagerConnectionDelegate
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
 
        guard let attempt = connectionAttemptForPeripheral(peripheral) else {
            // http://stackoverflow.com/questions/11557500/corebluetooth-central-manager-callback-diddiscoverperipheral-twice
            return
        }
        succeedConnectionAttempt(attempt)
        LYCentral.shared.state = .LYBluetoothStateConnected
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: LYBluetoothConnectedNotification), object: nil, userInfo: nil)

        print("连接成功")
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {

        guard let attempt = connectionAttemptForPeripheral(peripheral) else {
            return
        }
        failConnectionAttempt(attempt, error: .connectionByUUIDIsNotImplementedYet)
        print("连接失败")
        LYCentral.shared.state = .LYBluetoothStateDisconnected
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("断开连接")
        LYCentral.shared.state = .LYBluetoothStateDisconnected
        if let remotePeripheral = connectedRemotePeripherals.filter({ $0.peripheral == peripheral }).last {
            connectedRemotePeripherals.remove(at: connectedRemotePeripherals.firstIndex(of: remotePeripheral)!)
        }
    }
}
